#!/bin/sh
set -e

usage () {
    echo "usage: git cleanup [-nh]" >&2
    echo >&2
    echo "Deletes all branches that have already been merged into master, production, or canary." >&2
    echo "Removes those branches both locally and in the origin remote.  Will be " >&2
    echo "most conservative with deletions." >&2
    echo >&2
    echo "Options:" >&2
    echo "-n    Dry-run" >&2
    echo "-l    Local branches only, don't touch the remotes"
    echo "-h    Show this help" >&2
}

dryrun=0
remotes=1
while getopts nlh flag; do
    case "$flag" in
        n) dryrun=1;;
        l) remotes=0;;
        h) usage; exit 2;;
    esac
done
shift $(($OPTIND - 1))

#
# This will clean up any branch (both locally and remotely) that has been
# merged into any of the known "trunks".  Trunks are any of:
#
#   - master (local) + origin/master
#   - production (local) + origin/production
#   - canary (local) + origin/canary
#

safegit () {
    if [ "$dryrun" -eq 1 ]; then
        echo git "$@"
    else
        git "$@"
    fi
}

#
# The Algorithm[tm]:
# - Find the smallest set of common ancestors for those trunks.  (There can
#   actually be multiple, although unlikely.)
# - For each local branch, check if any of the common ancestors contains it,
#   but not vice-versa (prevents newly-created branches from being deleted)
# - Idem for each remote branch
#

find_common_base () {
    if [ $# -eq 1 ]; then
        git sha "$1"
    else
        git merge-base "$1" "$2"
    fi
}

find_branch_base () {
    branch="$1"
    base_point=""

    if git local-branch-exists "$branch"; then
        base_point=$(find_common_base "$branch" $base_point)
    fi

    if git remote-branch-exists origin "$branch"; then
        base_point=$(find_common_base "origin/$branch" $base_point)
    fi

    if [ -n "$base_point" ]; then
        echo "$base_point"
    fi
}


find_bases () {
    find_branch_base master
    find_branch_base production
    find_branch_base canary
}

bases=$(find_bases)

for branch in $(git local-branches          \
                | grep -vxF 'master'        \
                | grep -vxF 'production'    \
                | grep -vxF 'canary'); do
    for base in $bases; do
        if git contains "$base" "$branch"; then
            if ! git contains "$branch" "$base"; then
                # Actually delete
                if ! safegit branch -D "$branch"; then
                    echo "Errors deleting local branch $branch" >&2
                fi
                break
            fi
        fi
    done
done

# Pruning first will remove any remote tracking branches that don't exist in
# the remote anymore anyway.

#XXX: FIXME: This gave trouble, as it tried to remove branches from Heroku remotes... :(
#for remote in $(git remote); do
for remote in origin; do
    safegit remote prune "$remote" >/dev/null 2>/dev/null

    if [ $remotes -eq 1 ]; then
        branches_to_remove=""
        for branch in $(git remote-branches "$remote" | grep -vEe '/(master|production|canary)$'); do
            for base in $bases; do
                if git contains "$base" "$branch"; then
                    if ! git contains "$branch" "$base"; then
                        branchname=$(echo "$branch" | cut -d/ -f2-)
                        branches_to_remove="$branches_to_remove $branchname"
                        break
                    fi
                fi
            done
        done

        if [ -n "$branches_to_remove" ]; then
            if ! safegit push "$remote" --delete $branches_to_remove; then
                echo "Errors deleting branches $branches_to_remove from remote '$remote'" >&2
            fi
        fi
    fi
done

# Delete any remaining local remote-tracking branches of remotes that are gone
# This is an atypical situation that has occurred to me personally after having
# used the command:
#
#     $ hub merge <some-github-url>
#
branches_to_remove=""
for branch in $(git remote-branches); do
    for base in $bases; do
        if git contains "$base" "$branch"; then
            if ! git contains "$branch" "$base"; then
                branches_to_remove="$branches_to_remove $branch"
                break
            fi
        fi
    done
done

if [ -n "$branches_to_remove" ]; then
    safegit branch -dr $branches_to_remove
fi
